#!/usr/bin/python2.5
# Copyright 2009 Google Inc.
# 
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 
#      http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""Geocode utility functions."""

import logging
from xml.dom import minidom
from google.appengine.api import memcache
from google.appengine.api import urlfetch
from google.appengine.ext import db
from google.appengine.ext import webapp
import wsgiref.handlers
from time import sleep

class GeoLocation(db.Model):
  """A location on the map."""
  address = db.StringProperty(required=True)
  complete = db.BooleanProperty(default=False)
  lat = db.FloatProperty()
  longt = db.FloatProperty()
  approx = db.BooleanProperty(required=True)


def GetUnknown():
  """Return entries that are unknown."""
  return GeoLocation.all().filter("complete =", False).fetch(100)


def UpdateUnknown(address, lat, longt, approx):
  """Update an unknown entry."""
  item = GeoLocation.all().filter("address =", address.lower()).get()
  if not item:
    item = GeoLocation(address=address.lower())
  item.lat = lat
  item.longt = longt
  item.approx = approx
  item.put()
  memcache.set(address.lower(), "%s,%s,%s,%s" % (lat, longt, approx, True))


def NewItem(address, lat, longt, approx, complete=True):
  """Create a new geolocation."""
  address = address.lower()
  logging.info("Writing new location %s: %r %r %s %s", address, lat, longt,
               approx, complete)
  existing = GeoLocation.all().filter("address = ", address).fetch(100)
  for item in existing:
    item.delete()
  item = GeoLocation(address=address, approx=False)
  item.lat = lat
  item.longt = longt
  item.approx = approx
  item.complete = complete
  item.put()
  memcache.set(address, "%s,%s,%s,%s" % (lat, longt, approx, complete))


def GetItem(address, flush=False):
  """Fetch item from memcache or from the datastore.
     Args:
       address: the address to fetch
       flush: if true, flush from memcache

     Returns:
       GeoLocation object
  """
  address = address.lower()
  flushed = False
  cached = memcache.get(address.lower())
  if cached:
    lat, longt, approx, complete = cached.split(",")
    if complete == "False":
      # don't have an entry for this one.
      if flush:
        logging.info("flushing %s", address)
        memcache.delete(address.lower())
        flushed = True
    else:
      lat = float(lat)
      longt = float(longt)
      approx = approx == "True"
      if abs(lat) < 0.01 or abs(longt) < 0.01:
        memcache.delete(address.lower())
      else:
        return (lat, longt, approx)
  item = GeoLocation.all().filter("address =", address.lower()).get()
  if item:
    logging.debug("got item from datastore for %s %s %s %s", address,
                  item.complete, item.lat, item.longt)
  else:
    logging.debug("couldnt find item in datastore for %s", address)
  if not item or not item.complete:
    logging.debug("invalid item for %s", address)
    return None
  if flushed:
    memcache.set(address, "%s,%s,%s,%s" % (item.lat, item.longt, item.approx,
                                           True))
  return (item.lat, item.longt, item.approx)


def GetLocation(address, state="victoria", flush=False, flip_address=True,
                country='Australia'):
  """Get a location from either the datastore or geocoding."""
  address = address.lower()
  cached = GetItem(address, flush=flush)
  if cached:
    return cached
  logging.debug("couldn't find cached data for %s", address)
  # we have to geolocate
  # TODO(bst): Move the flipping out to the CFA module
  if flip_address:
    geoaddress = (", ".join(reversed(address.split(","))) + ", " + state + ', ' +
        country)
  else:
    geoaddress = (address + ", " + state + ', ' + country)
  lat, longt = GoogleGeoCode(geoaddress)
  if lat is None:
    logging.info("first attempt at geocode failed for %r", address)
    if '/' in address:
      # thank you very much SA CFS
      geoaddress = address.split("/")[0] + ", " + state
    else:
      # try again, just town name
      geoaddress = address.split(",")[0] + ", " + state
    lat, longt = GoogleGeoCode(geoaddress)
    if lat is None:
      # couldn't even find town name
      if address.startswith('dse'):
        # DSE addresses in a different format
        location, geoaddress = address.split(',', 1)
        if '-' in geoaddress:
          town, road = geoaddress.split('-', 1)
          logging.info('DSE hack - trying %s %s %s', road, town, state)
          lat, longt = GoogleGeoCode('%s %s %s' % (road, town, state))
        elif '  ' in geoaddress:
          town, road = geoaddress.split('  ', 1)
          logging.info('DSE hack #2 - trying %s %s %s', road, town, state)
          lat, longt = GoogleGeoCode('%s %s %s' % (road, town, state))
        else:
          lat, longt = None, None
        if lat is None:
          approx, complete = False, False
          lat, longt = 0.0, 0.0
        else:
          approx, complete = False, True
      else:
        approx, complete = False, False
    else:
      approx, complete = True, True
  else:
    approx, complete = False, True
  logging.debug("GetLocation writing new item for %s %s %s", address, lat,
                longt)
  NewItem(address, lat, longt, approx=approx, complete=complete)
  return (lat, longt, approx)


def GoogleGeoCode(address):
  """Geocode an address in CFA format."""

  retries = 3
  delay = 0.2

  logging.info('Geocoding %s', address)
  location = address.replace(" ", "%20")
  # TODO(bst): Hide our API key!
  key = ("ABQIAAAA-O3c-Om9OcvXMOJXreXHAxRlOb26qSyU154aZeLwOrF4C7-"
         "DphSw7JoLuXrcDjUb_7h-S1txkSCY7A")
  url = "http://maps.google.com/maps/geo?q="+location +"&output=xml&key="+key

  for attempt in xrange(retries):
    result = urlfetch.fetch(url)
    try:
      dom = minidom.parseString(result.content)
    except:  # gah.
      logging.warning("bad geocode response for '%r' - '%r'",
                      location, result.content)
      return None, None
    geo_status = int(dom.getElementsByTagName("code")[0].childNodes[0].data)
    if geo_status == 200:
      coord = dom.getElementsByTagName("coordinates")[0].childNodes[0].data
      logging.debug("COORD is %r", coord)
      lat, longt, _ = coord.split(",")
      lat, longt = float(lat), float(longt)
      if abs(lat) < 0.01:
        logging.warning('Bad latitude.')
        return None, None
      return lat, longt
    elif geo_status == 620: # ratelimited
      logging.warning('Geocoding ratelimited. Sleeping for %.1f seconds.',
                      delay)
      if attempt < (retries - 1):
        sleep(delay)
      delay *= 2
    else:
      logging.warning('Geocoding for %s returned %s', address, geo_status)
      return None, None

  logging.warning('Aborting geocoder after all retries exhausted.')
  return None, None


class GeocodeHandler(webapp.RequestHandler):
  """Do a geocode."""

  def get(self):
    """GET handler."""
    address = self.request.get("address")
    longt, lat, approx = GetLocation(address)
    if lat is not None:
      self.response.out.write("%.6f %.6f %s"%(lat, longt, approx))
    memcache.delete(address.lower())


class GeocodeUploadHandler(webapp.RequestHandler):
  """Upload a bunch of records."""

  def post(self):
    """POST handler."""
    body = self.request.get("data")
    for line in body.split("\n"):
      if not line.strip():
        continue
      els = [x.strip('" ') for x in line.split(';')]
      try:
        _, street, town, lat, longt, approx = els
      except ValueError:
        raise ValueError("got %r from %r"%(els, line))
      approx = bool(approx.strip())
      lat, longt = float(lat), float(longt)
      if street.strip():
        address = "%s, %s" % (town, street)
      else:
        address = town
      cached = GetItem(address)
      if cached:
        logging.debug("already have %s: %r", address, cached)
      if abs(lat) < 0.01 or abs(longt) < 0.01:
        logging.debug("bogus entries for %s", address)
        #NewItem(address.lower(), 0.0, 0.0, False, complete=False)
        continue
      else:
        logging.debug("adding new entry for %s", address)
        NewItem(address.lower(), lat, longt, approx, complete=True)
    self.response.out.write("done")


class GeocodeFlushHandler(webapp.RequestHandler):
  """flush memcache."""

  def get(self):
    """GET handler."""
    memcache.flush_all()


class GeocodeEditHandler(webapp.RequestHandler):
  """Edit existing/broken records."""

  def get(self):
    """GET handler."""
    if self.request.get("address"):
      address = self.request.get("address")
      fix = GeoLocation.all().filter('address =',
                                     self.request.get('address')).get()
      fix.lat = float(self.request.get('lat'))
      fix.longt = float(self.request.get('longt'))
      fix.approx = bool(self.request.get('approx'))
      fix.complete = True
      fix.put()
      memcache.delete(address)

    missing = GeoLocation.all().filter('complete =', False).fetch(9000)
    self.response.out.write("<html><body>")
    for i in missing:
      self.response.out.write("<form>")
      self.response.out.write("<b>"+ i.address + "</b><br/>")
      self.response.out.write('<input type=hidden name=address value="%s">'%(
          i.address))
      self.response.out.write("lat: <input type=text name=lat>")
      self.response.out.write("long: <input type=text name=long>")
      self.response.out.write("approx? <input type=checkbox name=approx>")
      self.response.out.write("<input type=submit name=submit value=Update>")
      self.response.out.write("</form><p/>")


def main():
  app = webapp.WSGIApplication([
      ('/geocode/query', GeocodeHandler),
      ('/geocode/upload', GeocodeUploadHandler),
      ('/geocode/edit', GeocodeEditHandler),
      ('/geocode/flush', GeocodeFlushHandler),
      ], debug=True)
  wsgiref.handlers.CGIHandler().run(app)

if __name__ == '__main__':
  main()
